import * as babel from 'babel-core';
import randomstring from 'randomstring';
import * as t from 'babel-types';
import traverse from "babel-traverse";
import * as babylon from "babylon";

Error.stackTraceLimit = Infinity;

let externs = ["events", "Math", "Date", "Array", "console", "persistent", "Instance", "require", "module", "arguments", "setTimeout","isNaN"];
let inputs = ["events", "persistent", "Instance"];

function buildPersistent(source) {
    const ast = babylon.parse(source, {});

    let ts = (new Date).getTime();

    console.log("Preprocessing functions")
    let functions = [];
    let closures = {};
    let declarations = {};
    let args = {};
    let functionExpressionDecs =[];
    let requiredFuns=[];

    let rootPath;
    let i = 0;
    function createOrPush(what,arrName,value){
        if (what[arrName])
            what[arrName].push(value);
        else
            what[arrName]=[value];
    }
    function addFunID(path){
        let fpNode = path.getFunctionParent().node;
        if (typeof fpNode.funName === "undefined")
            fpNode.funName = "_$root";
        if (!path.parentPath.isVariableDeclarator()) {
            let tmp = functionExpressionDecs[fpNode.funName];
            let obj = {name: path.node.id.name, funName: path.node.funName};
            if (!tmp) functionExpressionDecs[fpNode.funName] = [obj];
            else tmp.push(obj);
        }
        //return; //could be useless...
        if (typeof declarations[fpNode.funName] === "undefined")
            declarations[fpNode.funName]=[path.node.id.name];
        else
            declarations[fpNode.funName].push(path.node.id.name);
    }
    function naming(originalName){
        let funName = 'Lambda_' + randomstring.generate()
        if (typeof originalName!=="undefined")
        if (originalName.split('')[0] !== "$")
            funName= `${originalName}_${ts}_${++i}`;
        else
            funName= `P${originalName}`;
        return funName;
    }
    function parseFunction(path){
        let funName, node=path.node;
        functions.push(node.funName = funName = naming(node.id?node.id.name:undefined));
        args[funName] = [];
        for (let param of node.params)
            args[funName].push(param.name);
        createOrPush(requiredFuns,path.getFunctionParent().node.funName,`"${funName}"`);
    }
    let visitors = {
        FunctionDeclaration(path) {
            parseFunction(path);
            if (path.node.id) addFunID(path);
        },
        FunctionExpression(path) {
            parseFunction(path);
            if (path.node.id) addFunID(path);
        },
        ArrowFunctionExpression:parseFunction,
        Identifier(path) { // todo: better code...
            let fpNode = path.getFunctionParent().node;
            if (path.parentPath && (path.parentPath.isVariableDeclarator() && path.key == "id"
                    || (path.parentPath.parentPath && path.parentPath.parentPath.parentPath &&
                        path.parentPath.parentPath.parentPath.isVariableDeclarator() && path.parentPath.parentPath.key == "id")
                    || path.parentPath.isArrowFunctionExpression()
                    || path.parentPath.isFunctionExpression()
                    || path.parentPath.isFunctionDeclaration())) {
                if (typeof fpNode.funName === "undefined")
                    fpNode.funName = "_$root";
                let tmp = declarations[fpNode.funName];
                if (!tmp) declarations[fpNode.funName] = [path.node.name];
                else tmp.push(path.node.name);
            }
            else if (!(path.parentPath && path.parentPath.isMemberExpression() && path.key == "property" && !path.parentPath.node.computed) &&
                !(path.key === "key" && path.parentPath.type == "ObjectProperty") &&
                !(path.key === "param" && path.parentPath.type == "CatchClause")) {
                if (declarations[fpNode.funName] && declarations[fpNode.funName].includes(path.node.name))
                    return;
                if (closures[fpNode.funName] && closures[fpNode.funName].includes(path.node.name))
                    return;
                if (externs.indexOf(path.node.name) != -1)
                    return;
                if (typeof fpNode.funName === "undefined")
                    fpNode.funName = "_$root";
                let tmp = closures[fpNode.funName];
                if (!tmp) closures[fpNode.funName] = [path.node.name];
                else tmp.push(path.node.name);
            }
        },
        Program:{
            enter(path) {
                rootPath = path;
            },
            exit(path){
                if (functionExpressionDecs["_$root"]) {
                    let decs=[];
                    for (let aDec of functionExpressionDecs["_$root"])
                        decs.push(`${aDec.name}=persistent.funClasses["${aDec.funName}"](_$instance)`)
                    let rootDecStr = `let ${decs.join(",")};`;
                    path.unshiftContainer("body", babylon.parse(rootDecStr).program.body);
                }
            }
        }
    };

    traverse(ast, visitors);
    console.log('functions:', functions);
    console.log('params:', args);
    console.log('closure:', closures);
    console.log('decs:', declarations);
    console.log(`fun decs:`, functionExpressionDecs);

//==========

// Generate function templates;
    console.log("Generating function templates");

    const transG = {
        enter(path) {
            if (path.node.skip)
                path.skip();
        },
        exit(path) {
            rootPath.unshiftContainer('body', babylon.parse(`persistent.funClasses["${path.node.funName}"]=${path.node.funName}`).program.body);
            rootPath.unshiftContainer('body', genFunObj(path.node.funName, path.node.body, path.node.id));

            if (path.node.id) {
                path.replaceWith(t.Identifier(path.node.id.name));
                //path.replaceWithMultiple(babylon.parse(`${path.node.id.name}=persistent.funClasses["${path.node.funName}"](_$instance);`).program.body);
            }
            else
                path.replaceWith(babylon.parse(`(persistent.funClasses["${path.node.funName}"](_$instance))`).program.body[0]);
        }
    };
    const visitorsG = {
        FunctionDeclaration: transG,
        FunctionExpression: transG,
        ArrowFunctionExpression: transG,
        ReturnStatement(path) {
            if (path.node.skipRet)
                return;
            let arr=babylon.parse(`()=>{
            _$instance.notify();
            return ${path.getFunctionParent().node.funName}_retVal;}`).program.body[0].expression.body.body;
            arr[1].skipRet=true;
            //console.log(arr);
            if (path.node.argument !== null)
                arr.unshift(t.ExpressionStatement(
                    t.AssignmentExpression("=", t.identifier(`${path.getFunctionParent().node.funName}_retVal`), (path.node.argument))));
            path.replaceWithMultiple(arr);
        }
    };

    function genFunObj(funName, body, originalName) {

        let decStrs = [];
        if (declarations[funName])
            for (let aVar of declarations[funName])
                decStrs.push(aVar);
        let cloStrs = [];
        if (closures[funName])
            for (let aVar of closures[funName])
                cloStrs.push(aVar);
        let funDecStr="";
        if (functionExpressionDecs[funName]) {
            let decs=[];
            for (let aDec of functionExpressionDecs[funName])
                decs.push(`${aDec.name}=persistent.funClasses["${aDec.funName}"](_$instance)`)
            funDecStr = `let ${decs.join(",")};`;
        }
        let originalNameDec="";
        if (originalName) originalNameDec=`let ${originalName.name}=${funName}_I;`;
        let myVarUp=[];
        if (declarations[funName])
            for (let aVar of declarations[funName])
                myVarUp.push(`${aVar}=myVars.${aVar}`);
        const templateFunObj = `
    function ${funName}(upinstance){
        let ${funName}_I=function(${args[funName].join(",")}){
            //console.log("==> run ${funName}");
            let _$instance=new Instance(upinstance);
            _$instance.notify=function(){
                //console.log("${funName} notified");
                _$instance.setSelf({${decStrs.join(",")}}); //sync latest value to instance
                if (upinstance.notify)
                    upinstance.notify();
                updateClosure();
            }
            _$instance.decUpdate=function(){
                let myVars=_$instance.getSelf();
                ${myVarUp.join(";")}
            }
            getUpinstance();
            let ${funName}_retVal;
            ${funDecStr}
            ${originalNameDec}
            _$transformed_body;
            _$instance.notify();
            return ${funName}_retVal;
        }
        _$closure_names;
        function getUpinstance(){
            if (upinstance.notify)
                upinstance.notify();
            let closure=upinstance.get();
            //console.log("${funName} closure: ",closure);
            _$closure_put_value;
        };
        function updateClosure(){
            //console.log('${funName} update instance:',{${cloStrs.join(",")}});
            upinstance.setRecur({${cloStrs.join(",")}});
            if (upinstance.decUpdate)
                upinstance.decUpdate();
        }
        ${funName}_I.__P$id__=parseInt(Math.random()*1000000000000);
        ${funName}_I.__P$info = {id:${funName}_I.__P$id__, name:"${funName}",
            instance:upinstance, code:${funName}.toString(), 
            requireFunClass:[${requiredFuns[funName]?requiredFuns[funName].join(","):""}]
        };
        persistent.loadedFunctions[${funName}_I.__P$id__]=${funName}_I;
        if (typeof persistent.funClasses["${funName}"]==="undefined")
            persistent.funClasses["${funName}"]=${funName};
        return ${funName}_I;
    }`;
        let ast = babylon.parse(templateFunObj);
        let retAst;
        try {
            traverse(ast, {
                Identifier(path) {
                    let name = path.node.name;
                    if (name === '_$transformed_body') {
                        if (body.type === "BlockStatement")
                            path.parentPath.replaceWithMultiple(body.body);
                        else
                            path.parentPath.replaceWith(body);
                    } else if (name === "_$all_variables") {
                        let strs = [], vars = {};
                        if (closures[funName])
                            for (let aVar of closures[funName])
                                vars[aVar] = true;
                        if (declarations[funName])
                            for (let aVar of declarations[funName])
                                vars[aVar] = true;
                        for (let aVar in vars)
                            strs.push(aVar + ":" + aVar);
                        path.replaceWith(babylon.parse(`({${strs.join(",")}})`).program.body[0]);
                    } else if (name === "_$closure_put_value") {
                        let strs = [];
                        if (closures[funName]) {
                            for (let aVar of closures[funName])
                                strs.push(`${aVar}=closure.${aVar}`);
                        }
                        path.parentPath.replaceWithMultiple(babylon.parse(`${strs.join(";")}`).program.body);
                    } else if (name === "_$closure_names") {
                        let strs = [];
                        if (closures[funName]) strs = closures[funName];
                        //console.log('strs',strs)
                        if (strs.length === 0)
                            path.remove();
                        else
                            path.replaceWithMultiple(babylon.parse(`let ${strs.join(",")};`).program.body);
                    }
                },
                FunctionDeclaration(path) {
                    if (!retAst) {
                        retAst = path.node;
                        retAst.skip = true;
                        path.node.id.name = funName;
                        return;
                    }
                },
            });
            return retAst;
        }catch(e){
            console.log(templateFunObj);
            throw e;
        }
    }

    traverse(ast, visitorsG);

    let {code, map, ast2} = babel.transformFromAst(ast, code);

    let rootDecVars = [];
    if (declarations["_$root"]){
        for (let aVar of declarations["_$root"])
            rootDecVars.push(aVar + ":" + aVar);
    }
    code = `

export default (_$instance, _$inputs)=>{
    let {${inputs.join(",")}} = _$inputs;    
    _$instance.notify=function(){
       //console.log("root notified");
        _$instance.setSelf({${rootDecVars.join(",")}});
    }
    ${code}
    _$instance.notify();
}`;

    return code;
}

export default buildPersistent;